package com.hzw.saas.service.workspace.service;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hzw.saas.api.product.IProductService;
import com.hzw.saas.api.user.IUserRightsService;
import com.hzw.saas.api.user.bo.UserRightsBO;
import com.hzw.saas.api.workspace.IWorkInstanceService;
import com.hzw.saas.api.workspace.IWorkRecordsService;
import com.hzw.saas.api.workspace.bo.WorkInstanceBo;
import com.hzw.saas.api.workspace.bo.WorkInstanceInfoBo;
import com.hzw.saas.api.workspace.enms.InstStatus;
import com.hzw.saas.common.config.annotation.RedisProcessLock;
import com.hzw.saas.common.config.constants.RedisConsts;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.config.util.PageParam;
import com.hzw.saas.common.config.util.PageUtils;
import com.hzw.saas.common.config.util.RedisUtil;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.SnowFlakeUtil;
import com.hzw.saas.common.util.UnitConvertUtils;
import com.hzw.saas.common.util.enums.TimeUnitEnum;
import com.hzw.saas.service.workspace.mapper.AwsEcsConfigMapper;
import com.hzw.saas.service.workspace.mapper.WorkInstanceMapper;
import com.hzw.saas.service.workspace.pojo.event.*;
import com.hzw.saas.service.workspace.pojo.model.AwsEcsConfig;
import com.hzw.saas.service.workspace.pojo.model.WorkInstance;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * <p>
 * 用户工作实例表 服务实现类
 * </p>
 *
 * @author sonam
 * @since 2021-02-03
 */
@Service("workInstanceServiceImpl")
@RequiredArgsConstructor
@Slf4j
public class WorkInstanceServiceImpl extends ServiceImpl<WorkInstanceMapper, WorkInstance> implements IWorkInstanceService, IService<WorkInstance> {

    private final IProductService productService;
    private final IUserRightsService userRightsService;
    private final AwsEcsConfigMapper awsEcsConfigMapper;
    private final ApplicationEventPublisher applicationEventPublisher;

    @Resource(name = "workInstanceServiceImpl")
    private IWorkInstanceService self;

    @Resource(name = "workRecordsServiceImpl")
    private IWorkRecordsService workRecordsService;

    @Override
    public WorkInstanceBo getWorkInstanceById(String instId) {
        WorkInstance workInstance = this.getById(instId);
        return MapperUtil.nf().map(workInstance, WorkInstanceBo.class);
    }

    @Override
    @SneakyThrows
    @Transactional
    @RedisProcessLock(lockName = "CreateWorkInstance", key = "#userId + ':' + #productId")
    public WorkInstanceBo createWorkInstance(String userId, String productId, boolean repeat) {
        AssertUtil.assertThrow("开通失败，产品ID不能为空", StringUtil.isBlank(productId));
        /** 查询product对应的配置信息*/
        AwsEcsConfig awsEcsConfig = awsEcsConfigMapper.selectOne(Wrappers.<AwsEcsConfig>lambdaQuery().eq(AwsEcsConfig::getProductId, productId));
        AssertUtil.assertThrow(String.format("创建任务定义失败，产品[%s]找不到配置", productId), Objects.isNull(awsEcsConfig));

        /** 检查工作实例全新并返回过期时间*/
        String recycleDuration = checkUserRights(userId);

        /** 当前产品运行中的数量*/
        int runCount = this.count(Wrappers.<WorkInstance>lambdaQuery()
            .eq(WorkInstance::getUserId, userId)
            .eq(WorkInstance::getProductId, productId)
            .eq(WorkInstance::getCluster, awsEcsConfig.getCluster())
            .and(wrapper -> wrapper.ne(WorkInstance::getInstTaskId, "").isNotNull(WorkInstance::getInstTaskId)));
        /** 已开启相同的实例，不可多次开启*/
        AssertUtil.assertThrow("已开启相同的实例，不可多次开启！",  runCount > 0 && !repeat);

        /** 查询已创建过的工作实例*/
        List<WorkInstance> stopWorkInstances = this.list(Wrappers.<WorkInstance>lambdaQuery()
            .eq(WorkInstance::getUserId, userId)
            .eq(WorkInstance::getProductId, productId)
            .eq(WorkInstance::getCluster, awsEcsConfig.getCluster())
            .and(wrapper -> wrapper.eq(WorkInstance::getInstTaskId, "").or().isNull(WorkInstance::getInstTaskId)).last(" limit 1"));

        WorkInstance workInstance = new WorkInstance();
        if(CollectionUtils.isEmpty(stopWorkInstances)) {
            /** 构造任务定义*/
            String instId = String.valueOf(SnowFlakeUtil.nextIdStr());
            workInstance.setInstId(instId);
            workInstance.setUserId(userId);
            workInstance.setInstStatus(0);
            workInstance.setProductId(productId);
            workInstance.setInstCreateTime(new Date());

            RegisterWorkInstanceEvent workInstanceEvent = new RegisterWorkInstanceEvent(workInstance, awsEcsConfig);
            applicationEventPublisher.publishEvent(workInstanceEvent);
            /** 注册成功拿到任务定义的revision*/
            workInstance = workInstanceEvent.getWorkInstance();
            /** 保存任务工作示例*/
            AssertUtil.assertThrow("开通工作空间失败", !save(workInstance));
        } else {
            workInstance = stopWorkInstances.get(0);
        }

        /** 最后开启工作实例*/
        WorkInstanceBo workInstanceBo = this.startWorkInstance(workInstance.getInstId());
        workInstance.setInstTaskId(workInstanceBo.getInstTaskId());
        log.info("工作实例启动成功：{}-{}:{}", workInstance.getInstTaskId(), workInstance.getInstTaskFamily(), workInstance.getInstTaskRevision());

        /** 设置docker回收时间*/
        long recycleSeconds = UnitConvertUtils.convertDatetime(recycleDuration, TimeUnitEnum.SECOND).longValue();
        this.setWorkInstanceExpire(workInstance.getInstId(), recycleSeconds);

        return MapperUtil.nf().map(workInstance, WorkInstanceBo.class);
    }

    private String checkUserRights(String userId) {
        /** 获取用户权限*/
        UserRightsBO userRightsByUser = userRightsService.getUserRightsByUser(userId);
        Optional<UserRightsBO> userRights = Optional.ofNullable(userRightsByUser);
        /** 判断aws是否过期*/
        AssertUtil.assertThrow("找不到用户权限",userRightsByUser == null);
        AssertUtil.assertThrow("云控制台已过期",userRightsByUser.getAwsCoreTimeExpireTime().before(DateUtil.date(new Date())));
        /** 判断aws余额是否大于*/
        AssertUtil.assertThrow("云控制台时间不足",!workRecordsService.ifOverTime(userId));
        /** 检查docker开启限制*/
        Integer dockerNumber = userRights.map(UserRightsBO::getDockerNumber).orElse(0);
        int workInstanceRunCount = this.getWorkInstanceRunCount(userId);
        AssertUtil.assertThrow(String.format("已开启%d个实例，不可多次开启！", dockerNumber), ObjectUtil.compare(workInstanceRunCount, dockerNumber) >= 0);

        return userRights.map(UserRightsBO::getDockerRecycleDuration).orElse("");
    }

    @Override
    public void setWorkInstanceExpire(String instId, long time) {
        if (StringUtils.isBlank(instId))
            return;
        String expireKey = RedisConsts.GROUP_LISTEN_EXPIRE_TRIGGER + instId;
        String key = RedisConsts.GROUP_LISTEN_EXPIRE_EVENT + instId;
        if (time <= 0) {
            log.debug("删除工作空间({})redis记录", instId);
            RedisUtil.del(key);
            RedisUtil.del(expireKey);
        } else {
            log.debug("工作空间({})开始计时：{}", instId, time);
            RedisUtil.set(key, new DeleteWorkInstanceEvent(new WorkInstance().setInstId(instId)));
            RedisUtil.set(expireKey, "work instance expire", time);
        }
    }

    @Override
    public long getWorkInstanceExpire(String instId) {
        if (StringUtils.isBlank(instId))
            return 0L;
        String expireKey = RedisConsts.GROUP_LISTEN_EXPIRE_TRIGGER + instId;
        return RedisUtil.getExpire(expireKey);
    }

    @Override
    public IPage<WorkInstanceBo> getAllWorkInstance(PageParam pageParam, String userId) {
        IPage<WorkInstance> workInstancePage = this.page(pageParam.convert(), Wrappers.<WorkInstance>lambdaQuery()
            .eq(StringUtil.isNotBlank(userId), WorkInstance::getUserId, userId)
            .and(wrapper -> wrapper.ne(WorkInstance::getInstTaskId, "")
                .isNotNull(WorkInstance::getInstTaskId)).orderByDesc(WorkInstance::getInstCreateTime));
        IPage<WorkInstanceBo> workInstanceDtoPage = PageUtils.convert(workInstancePage, WorkInstanceBo.class);
        /** 进程名称从哪里获取? 默认为产品名称*/
        workInstanceDtoPage.getRecords().forEach(workInstanceDto -> {
            workInstanceDto.setProcessName(productService.getProductName(workInstanceDto.getProductId()));
            if (Objects.nonNull(workInstanceDto.getInstStartTime())) {
                workInstanceDto.setInstStartTimeMs(workInstanceDto.getInstStartTime().getTime());
            }
            if (Objects.nonNull(workInstanceDto.getInstLastTime())) {
                workInstanceDto.setInstLastTimeMs(workInstanceDto.getInstLastTime().getTime());
            }
        });
        return workInstanceDtoPage;
    }

    /**
     * @param instId
     */
    @Override
    public WorkInstanceBo startWorkInstance(String instId) {
        /** 查看当前工作实例是否存在*/
        WorkInstance workInstance = this.getById(instId);
        AssertUtil.assertThrow("启动失败，工作实例丢失", Objects.isNull(workInstance));

        /** 查看当前工作是实例是否启动*/
        String instTaskId = workInstance.getInstTaskId();
        AssertUtil.assertThrow("启动失败，当前实例已启动", HttpStatus.CREATED, StrUtil.isNotBlank(instTaskId));

        /** 开启工作实例*/
        StartWorkInstanceEvent startWorkInstanceEvent = new StartWorkInstanceEvent(workInstance);
        applicationEventPublisher.publishEvent(startWorkInstanceEvent);

        /** 更新启动时间*/
        workInstance.setInstStartTime(new Date());
        this.updateById(startWorkInstanceEvent.getWorkInstance());
        return MapperUtil.nf().map(workInstance, WorkInstanceBo.class);
    }

    @Override
    public void stopWorkInstance(String instId) {
        WorkInstance workInstance = this.getById(instId);
        AssertUtil.assertThrow("停止失败，工作实例丢失", Objects.isNull(workInstance));
        try {
            StopWorkInstanceEvent stopWorkInstanceEvent = new StopWorkInstanceEvent(workInstance);
            applicationEventPublisher.publishEvent(stopWorkInstanceEvent);
        } finally {
            this.setWorkInstanceExpire(instId, 0);
            // 更新工作实例的arn。
            workInstance.setInstTaskId("");  // 设置taskId为null，复用该工作实例
            workInstance.setInstLastTime(workInstance.getInstStartTime()); // 设置最近一次开启工作实例的时间
            workInstance.setInstStartTime(null); // 将启动时间设置为null
            this.updateById(workInstance);
        }
    }

    @Override
    public WorkInstanceInfoBo selectWorkInstance(String instId) {
        WorkInstance workInstance = this.getById(instId);

        SelectWorkInstanceEvent selectWorkInstanceEvent = new SelectWorkInstanceEvent();
        selectWorkInstanceEvent.setWorkInstance(workInstance);
        applicationEventPublisher.publishEvent(selectWorkInstanceEvent);

        WorkInstanceInfoBo workInstanceInfoDto = new WorkInstanceInfoBo(
            selectWorkInstanceEvent.getIp(),
            selectWorkInstanceEvent.getPort(),
            selectWorkInstanceEvent.getInstStatus().value(),
            selectWorkInstanceEvent.getInstStatus().text(),
            // 通用的nonvc访问url
//            String.format("%s:%s/vnc_lite.html?password=%s/",
//                selectWorkInstanceEvent.getIp(),
//                selectWorkInstanceEvent.getPort(),
//                WorkspaceUtil.getInsPwd(workInstance.getInstId())));
            String.format("%s:%s/vnc_lite.html?password=vncpassword",
                selectWorkInstanceEvent.getIp(),
                selectWorkInstanceEvent.getPort()));
        return workInstanceInfoDto;
    }

    @Override
    @Transactional
    public void deleteWorkInstance(String instId) {
        WorkInstanceInfoBo workInstanceInfoDto = selectWorkInstance(instId);
        InstStatus instStatus = InstStatus.instance(workInstanceInfoDto.getStatus());
        log.debug("删除工作空间: {}", instId);
        switch (instStatus) {
            case STARTING:
            case RUNNING:
                this.stopWorkInstance(instId);
                this.removeById(instId);
                this.setWorkInstanceExpire(instId, 0);
                break;
            case STOPPED:
                /** 删除工作空间 ???*/
                //  ...
                /** 删除工作实例记录*/
                this.removeById(instId);
                this.setWorkInstanceExpire(instId, 0);
                /** 删除任务定义*/
                //TODO 调用SDK（只能去控制台删除）
                break;
//            case STARTING:
//                AssertUtil.assertThrow("删除失败,当前工作实例正在启动", true);
//                break;
            case OTHER:
                AssertUtil.assertThrow("删除失败,当前工作实例繁忙", true);
                break;
            default:
                // ~ignore,impossibility
        }
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void deleteWorkInstanceForce(String instId) {
        this.removeById(instId);
    }

    @Override
    public int getWorkInstanceRunCount(String userId) {
        return this.count(Wrappers.<WorkInstance>lambdaQuery()
            .eq(WorkInstance::getUserId, userId)
            .and(wrapper -> wrapper.ne(WorkInstance::getInstTaskId, "")
                .isNotNull(WorkInstance::getInstTaskId)));
    }
}
